﻿using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using T0yK4T.Tools;
using T0yK4T.IO;
using T0yK4T.Cryptography;
using T0yK4T.Configuration;
using System.Net;
using System.Net.Sockets;

namespace ToyChat.Server
{
    /// <summary>
    /// The main class for running a chat server
    /// </summary>
    public class Server : ComponentBase, IServer
    {
        private CryptoConnectionFarm connectionFarm;
        private IDatabaseHandler dbHandler;
        private IServerSettings settings;
        private Dictionary<uint, List<IServerModule>> packetHandlers = new Dictionary<uint, List<IServerModule>>();
        private AssemblyLoader loader;
        private Dictionary<string, uint> nameTable = new Dictionary<string, uint>();
        private IUserHandler userHandler;
        private IChannelHandler channelHandler;
        uint lastType = (uint)InternalPacketTypes.LAST + 1;
        private Dictionary<string, IUser> authenticatedUsers = new Dictionary<string, IUser>();
        private static readonly ConfigurableValue<string> serverName = Config.GetValue<string>("servername", new StringConverter(), "Empty");
        private static readonly ConfigurableValue<string> serverMotD = Config.GetValue<string>("servermotd", new StringConverter(), "None");
        private static readonly string[] excludeAssemblies = new string[] { "MongoDB.dll", "MongoDB.Bson.dll", "MongoDB.Driver.dll", "protobuf-net.dll", "Skeinfish.dll", "T0yK4T.dll" };
        
        /// <summary>
        /// Initializes a new instance of ToyServer and sets the logger and settings to the specified values
        /// </summary>
        /// <param name="logger">The <see cref="ILogger"/> to use for logging purposes</param>
        /// <param name="settings">The settings for the server</param>
        public Server(ServerSettings settings)
        {
            this.settings = settings;
            if (this.settings.Validate())
                this.Init();
            else
                this.LogError("Unable to Initialize Server, invalid settings");
            this.LogInformation("Server Initialized");
        }

        /// <summary>
        /// Initializes various instance variables that are always intialized
        /// </summary>
        private void Init()
        {
            this.dbHandler = new DatabaseHandler(this.settings, this);
            this.connectionFarm = new CryptoConnectionFarm(this.settings.LocalEndpoint, this.OnAcceptConnection);
            this.userHandler = new UserHandler(this);
            this.channelHandler = new ChannelHandler(this);
            this.loader = new AssemblyLoader();
            IEnumerable<IServerModule> handlers = this.loader.LoadDirectory<IServerModule>(Environment.CurrentDirectory, excludeAssemblies);
            foreach (IServerModule handler in handlers)
            {
                if (handler.Types != null && handler.Types.Length > 0)
                    this.AddHandler(handler);
                else
                    base.LogWarning("Handler {0} does not have any message types defined", handler.GetType().FullName);
            }
        }

        /// <summary>
        /// Destructor... Cleaning up!
        /// </summary>
        ~Server()
        {
            if (this.connectionFarm != null && this.connectionFarm.IsRunning)
            {
                this.connectionFarm.Stop();
                this.connectionFarm = null;
            }
            GC.SuppressFinalize(this);
            GC.Collect();
        }

        /// <summary>
        /// Starts the server (Starts listening for connections)
        /// </summary>
        public void Start()
        {
            this.connectionFarm.Start();
        }

        /// <summary>
        /// Stops the server (stops listening for connections)
        /// <remarks>
        /// Please note that connected clients are not automatically removed by this method
        /// </remarks>
        /// </summary>
        public void Stop()
        {
            this.connectionFarm.Stop();
        }

        /// <summary>
        /// Called from a <see cref="User"/>instance when a user disconnects
        /// </summary>
        /// <param name="user">The user that disconnected</param>
        public void OnDisconnect(IUser user)
        {
            this.authenticatedUsers.Remove(user.Username);
            this.userHandler.RemoveUser(user);
            this.channelHandler.RemoveUser(user);
            if (this.UserDisconnected != null)
                this.UserDisconnected.Invoke(user);
        }

        /// <summary>
        /// Called from a <see cref="IUser"/> isntance when a packet has arrived through the connection contained within the instance
        /// </summary>
        /// <param name="user">The user that "sent" the packet</param>
        /// <param name="packet">The packet that was received</param>
        public void OnPacketReceived(IUser user, Packet packet)
        {
            List<IServerModule> handlers;
            if (packet.SourceUserID != user.UserID)
                packet.SourceUserID = user.UserID;
            ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
            {
                if (this.packetHandlers.TryGetValue(packet.TypeID, out handlers))
                {
                    foreach (IServerModule handler in handlers)
                    {
                        try { handler.OnPacket((Packet)o); }
                        catch (Exception er) { base.LogException(er, System.Reflection.MethodBase.GetCurrentMethod()); }
                    }
                }
            }), packet);
            if (!this.packetHandlers.ContainsKey(packet.TypeID))
            {
                if (packet.DestinationUserID == -1 && packet.ChannelID == -1 && this.IsDefined(PermissionFlags.CanBroadcast, user))
                    this.Broadcast(packet);
                else if (packet.DestinationUserID != -1 && packet.ChannelID == -1 && this.IsDefined(PermissionFlags.CanUnicast, user))
                {
                    IUser dstUser;
                    if (this.userHandler.TryGetUser(packet.DestinationUserID, out dstUser))
                        dstUser.Send(packet);
                }
                else if (packet.ChannelID != -1 && packet.DestinationUserID == -1)
                {
                    IChannel channel;
                    if (this.channelHandler.TryGetChannel(packet.ChannelID, out channel))
                        channel.Send(packet);
                }
            }
        }

        /// <summary>
        /// Broadcasts the specified packet to all connections currently established on the server
        /// </summary>
        /// <param name="packet">The packet to broadcast</param>
        public void Broadcast(Packet packet)
        {
            foreach (IUser user in this.authenticatedUsers.Values)
                try { user.Send(packet); }
                catch { }
        }

        private bool IsDefined(PermissionFlags flag, IUser user)
        {
            return (user.Permissions & flag) == flag;
        }

        /// <summary>
        /// Called from the <see cref="CryptoConnectionFarm"/> when a connection has been accepted
        /// </summary>
        /// <param name="connection">The connection that was accepted</param>
        private void OnAcceptConnection(CryptoConnection connection)
        {
            IUser user;
            Packet pkt;
            if (!this.userHandler.AddUser(connection, out user) || user == null)
            {
                pkt = PacketFactory.GenerateASCIIPacket("Unable to authenticate");
                connection.SendPacket(pkt);
                connection.Close();
            }
            else
            {
                this.OnUserConnected(user);
                this.authenticatedUsers.Add(user.Username, user);
            }
        }

        private void OnUserConnected(IUser user)
        {
            if (this.UserConnected != null)
                this.UserConnected.Invoke(user);
        }

        /// <summary>
        /// Gets an instance of <see cref="IServerSettings"/> that represents the current settings of this server
        /// </summary>
        public IServerSettings Settings
        {
            get { return this.settings; }
        }

        /// <summary>
        /// Gets the nametable that represents the current state of the internal packet id to name map
        /// </summary>
        public Dictionary<string, uint> IDMap
        {
            get { return this.nameTable; }
        }

        /// <summary>
        /// Can be called from modules to add packet handlers to the packet queue
        /// </summary>
        /// <param name="handler">The packet handler to add</param>
        public void AddHandler(IServerModule handler)
        {
            handler.Initialize(this);
            foreach (string type in handler.Types)
            {
                List<IServerModule> preList;
                uint actualType;
                if (!this.nameTable.TryGetValue(type, out actualType))
                {
                    actualType = lastType++;
                    this.nameTable.Add(type, actualType);
                }
                if (!this.packetHandlers.TryGetValue(actualType, out preList))
                {
                    preList = new List<IServerModule>();
                    this.packetHandlers.Add(actualType, preList);
                }
                handler.SetTypeID(type, actualType);
                preList.Add(handler);
            }
        }

        /// <summary>
        /// Gets or Sets (but see remarks) the <see cref="IDatabaseHandler"/> currently in use by the server
        /// <remarks>This value can only be set if <see cref="IServerSettings.AllowDBSwitch"/> is true</remarks>
        /// </summary>
        public IDatabaseHandler GlobalDBHandler
        {
            get { return this.dbHandler; }
            set
            {
                if (this.settings.AllowDBSwitch)
                    this.dbHandler = value;
                else
                    throw new NotSupportedException("Cannot switch database (not allowed)");
            }
        }

        /// <summary>
        /// Gets a reference to the <see cref="IUserHandler"/> implementation currently in use by the server
        /// </summary>
        public IUserHandler UserHandler
        {
            get { return this.userHandler; }
        }

        /// <summary>
        /// Gets a reference to the <see cref="IChannelHandler"/> implementation currently in use by the server
        /// </summary>
        public IChannelHandler ChannelHandler
        {
            get { return this.channelHandler; }
        }

        /// <summary>
        /// This event is fired whenever a user has completed authentication and is considered connected
        /// </summary>
        public event UserEventHandler UserConnected;

        /// <summary>
        /// This event is fired whenever a user disconnects from the server
        /// </summary>
        public event UserEventHandler UserDisconnected;

        private void OnServerInfoChanged()
        {
            if (this.ServerInfoChanged != null)
                this.ServerInfoChanged.Invoke();
        }

        /// <summary>
        /// Gets or Sets the name of this server
        /// </summary>
        public string Name
        {
            get
            {
                return serverName;
            }
            set
            {
                serverName.Value = value;
                serverName.Store();
                this.OnServerInfoChanged();
            }
        }

        /// <summary>
        /// This event is fired whenever the server's informations have changed (Currently only name and motd)
        /// </summary>
        public event Action ServerInfoChanged;

        /// <summary>
        /// Gets or Sets the MotD (Message of the Day) of the server
        /// </summary>
        public string MotD
        {
            get
            {
                return serverMotD;
            }
            set
            {
                serverMotD.Value = value;
                serverMotD.Store();
                this.OnServerInfoChanged();
            }
        }

        /// <summary>
        /// Checks the <see cref="PermissionFlags"/> for the specified user
        /// </summary>
        /// <param name="user">The user to check the flags for</param>
        /// <param name="flags">The flags to check for</param>
        /// <returns>True if the user has the specified permissions, otherwise false</returns>
        public bool CheckPermissions(IUser user, PermissionFlags flags)
        {
            return (user.Permissions & flags) == flags;
        }

        public void Dispose()
        {
            foreach (IServerModule mod in this.packetHandlers.Values.Aggregate<List<IServerModule>, List<IServerModule>>(new List<IServerModule>(), (acc, cur) =>
                {
                    foreach (IServerModule mod in cur.Where(im => !acc.Contains(im)))
                        acc.Add(mod);
                    return acc;
                }))
            {
                mod.Dispose();
            }
        }
    }

    /// <summary>
    /// A delegate used via <see cref="CryptoConnectionFarm"/> to notify a user of a new connection
    /// </summary>
    /// <param name="connection"></param>
    public delegate void OnConnectionAcceptedDelegate(CryptoConnection connection);

    /// <summary>
    /// A simple "listener"
    /// </summary>
    public class CryptoConnectionFarm : ComponentBase
    {
        private TcpListener listener;
        private bool run = false;
        private OnConnectionAcceptedDelegate OnConnectionAccepted;
        private object p_Lock = new object();

        /// <summary>
        /// Initializes a new instance of <see cref="CryptoConnectionFarm"/>
        /// </summary>
        /// <param name="localEndpoint"></param>
        /// <param name="onAccept"></param>
        public CryptoConnectionFarm(IPEndPoint localEndpoint, OnConnectionAcceptedDelegate onAccept)
        {
            this.listener = new TcpListener(localEndpoint);
            this.OnConnectionAccepted = onAccept;
        }

        /// <summary>
        /// Starts listening
        /// </summary>
        public void Start()
        {
            if (this.run)
                base.LogError("CryptoConnectionFarm is already running");
            else
            {
                this.run = true;
                this.listener.Start();
                this.listener.BeginAcceptSocket(new AsyncCallback(this.OnAcceptSocket), this.listener);
            }
        }

        /// <summary>
        /// Called via <see cref="TcpListener"/> when doing a <see cref="TcpListener.BeginAcceptSocket"/> call
        /// </summary>
        /// <param name="res"></param>
        private void OnAcceptSocket(IAsyncResult res)
        {
            lock (this.p_Lock)
            {
                try
                {
                    Socket s = this.listener.EndAcceptSocket(res);
                    CryptoConnection connection = new CryptoConnection(this.Logger, s);
                    this.OnConnectionAccepted.Invoke(connection);
                    if (this.run)
                        this.listener.BeginAcceptSocket(new AsyncCallback(this.OnAcceptSocket), this.listener);
                }
                catch (Exception e)
                {
                    base.LogException(e, System.Reflection.MethodBase.GetCurrentMethod());
                    if (this.run)
                        this.listener.BeginAcceptSocket(new AsyncCallback(this.OnAcceptSocket), this.listener);
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether or not the listener is running and actively accepting connections
        /// </summary>
        public bool IsRunning
        {
            get { return this.listener.Server.IsBound && this.run; }
        }

        /// <summary>
        /// Stops the internal connection listener
        /// </summary>
        public void Stop()
        {
            if (!this.run)
                base.LogWarning("CryptoConnectionFarm is not running");
            else
            {
                this.run = false;
                this.listener.Stop();
            }
        }
    }
}
